﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Data.OracleClient;


namespace Kit
{
    public class ApnsKit
    {
        public const string kApnsHostProduction = "gateway.push.apple.com";
        public const string kApnsHostSandbox = "gateway.sandbox.push.apple.com";
        public const int kApnsPort = 2195;

        private static TcpClient apnsClientSandboxInHouse = null;
        private static SslStream apnsStreamSandboxInHouse = null;
        private static TcpClient apnsClientProdInHouse = null;
        private static SslStream apnsStreamProdInHouse = null;

        private static TcpClient apnsClientSandboxAppStore = null;
        private static SslStream apnsStreamSandboxAppStore = null;
        private static TcpClient apnsClientProdAppStore = null; //ios.ccp (AppStore推播憑證)
        private static SslStream apnsStreamProdAppStore = null; //ios.ccp (AppStore推播憑證)
        private const int kSendTimeout = 3000;
        private const int kReceiveTimeout = 1000;

        /*
        private const string kP12FileProd = @"D:\CCPSIP\Push\CCPSIP_Push_2015-03_Prod.p12";//ios.ccp (自建企業平台推播憑證)
        private const string kP12FileSandbox = @"D:\CCPSIP\Push\CCPSIP_Push_2015-03_Dev.p12";//ios.ccp (自建企業平台推播憑證)
        private const string kP12PasswordProd = "mQHK7PFD52hfGDNJuSXK";
        private const string kP12PasswordSandbox = "YrfRqnz5ySLzJs4Y2Lvw";
        private const string kP12FileProd2 = @"D:\CCPSIP\Push\CCPSIP_Push_2014-10_Prod.p12";//ios.ccp (AppStore推播憑證)
        private const string kP12FileSandbox2 = @"D:\CCPSIP\Push\CCPSIP_Push_2014-10_Dev.p12";//ios.ccp (AppStore推播憑證)
        private const string kP12PasswordProd2 = "yyb3q2L79YY9KDmrV88PuJzy";
        private const string kP12PasswordSandbox2 = "C9mjBN8KL7vxZzPiKfRtkjA4";
        */

        private const string kSoundFileMsg = "m1.caf";

        /// <summary>取得Oracle連線字串</summary>
        private static string GetConnStr()
        {
            return "SERVER=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=192.168.52.195)(PORT=1521)))(CONNECT_DATA =(SERVER=DEDICATED)(SID=UNIDB)));uid=ISENDER;pwd=aKhJJFA2sWfEsQz0Zm5iwQi9gjiVND";
        }

        /// <summary>列舉憑證類別</summary>
        private enum CertificateType { INHOUSE, APPSTORE };

        /// <summary>列舉組態種類</summary>
        private enum ConfigStringType { PATH, PASSWORD };

        /// <summary>列舉憑證版本</summary>
        private enum CertificateVersion { PROD, SANDBOX };

        /// <summary>下底線字串</summary>
        private const string UnderLine = "_";

        /// <summary>取得憑證組態</summary>
        /// <param name="CT">憑證類別</param>
        /// <param name="CST">組態種類</param>
        /// <param name="CV">憑證版本</param>
        /// <returns>組態字串</returns>
        private static string GetCertificateConfig(CertificateType CT, ConfigStringType CST, CertificateVersion CV)
        {
            OracleConnection ConnOracle = new OracleConnection(GetConnStr());
            string cmd_all = "SELECT * FROM PUSH_SERVICE_CONFIG";
            OracleCommand Cmd = new OracleCommand(cmd_all, ConnOracle);

            ConnOracle.Open();
            OracleDataReader Reader = Cmd.ExecuteReader();
            string ReturnConfigString = null;
            if (Reader.Read())  //PUSH_SERVICE_CONFIG Table中只有一行資料
            {
                ReturnConfigString = Convert.ToString(Reader[CT.ToString() + UnderLine + CST.ToString() + UnderLine + CV.ToString()]);
            }
            ConnOracle.Dispose();

            return ReturnConfigString;
        }

        /// <summary>
        /// 發送訊息 Push 通知(自建企業平台推播憑證)
        /// </summary>
        /// <param name="msgType">訊息類型代碼</param>
        /// <param name="alertMsg">alert文字訊息</param>
        /// <param name="badgeNum">App數字標記</param>
        /// <param name="from">發訊方SIP號碼</param>
        /// <param name="pushToken">收訊方Push Token</param>
        /// <param name="isDev">是否為開發環境</param>
        /// <param name="packetID">Notification ID</param>
        /// <param name="expiryTime">Push通知過期時間</param>
        public static void SendMsgPushInHouse(string msgType, string alertMsg, UInt32? badgeNum, string from, string pushToken, bool isDev, UInt32 packetID, DateTime expiryTime)
        {
            //【01】Generate Data Packet
            //
            string soundFile = (string.IsNullOrEmpty(alertMsg) ? null : kSoundFileMsg);
            string payloadStr = getSipPayload(alertMsg, soundFile, badgeNum, from, msgType);
            byte[] apnPacket = ApnsKit.buildPacket(payloadStr, pushToken, packetID, expiryTime);

            //原始憑證推播
            for (int tryTimes = 1; tryTimes <= 3; tryTimes++)
            {
                try
                {
                    ConnectAPNsInHouse(isDev);

                    //【02】Send Data Packet
                    //
                    SslStream apnsStream = (isDev ? apnsStreamSandboxInHouse : apnsStreamProdInHouse);

                    apnsStream.Write(apnPacket);
                    apnsStream.Flush();

                    break;
                }
                catch (System.IO.IOException ex)
                {
                    if (ex.InnerException is SocketException)
                    {
                        SocketException sEx = (SocketException)ex.InnerException;

                        string debugStr = string.Format("({0}) tryTimes={1};", packetID, tryTimes);
                        debugStr += Environment.NewLine + ChrisKit.Ex2Xml(sEx);
                        debugStr += Environment.NewLine + Environment.NewLine;
                        Kit.ChrisKit.AppendToDayLog(debugStr);

                        if (sEx.SocketErrorCode == SocketError.ConnectionAborted)//SocketError=10053
                            continue;
                        else if (sEx.SocketErrorCode == SocketError.TimedOut)//SocketError=10060
                            continue;
                        else if (sEx.SocketErrorCode == SocketError.WouldBlock)//SocketError=10035
                            break;
                        else
                        {
                            continue;
                        }
                    }
                }
                catch (Exception ex)
                {
                    string errorMsg = Kit.ChrisKit.Ex2Xml(ex, true);
                    Kit.ChrisKit.AppendToDayLog(errorMsg);
                    Kit.ChrisKit.AppendToDayLog("[Payload]=" + payloadStr);
                }
            }//End of for
        }

        /// <summary>
        /// 發送訊息 Push 通知(AppStore推播憑證)
        /// </summary>
        /// <param name="msgType">訊息類型代碼</param>
        /// <param name="alertMsg">alert文字訊息</param>
        /// <param name="badgeNum">App數字標記</param>
        /// <param name="from">發訊方SIP號碼</param>
        /// <param name="pushToken">收訊方Push Token</param>
        /// <param name="isDev">是否為開發環境</param>
        /// <param name="packetID">Notification ID</param>
        /// <param name="expiryTime">Push通知過期時間</param>
        public static void SendMsgPushAppStore(string msgType, string alertMsg, UInt32? badgeNum, string from, string pushToken, bool isDev, UInt32 packetID, DateTime expiryTime)
        {
            //【01】Generate Data Packet
            //
            string soundFile = (string.IsNullOrEmpty(alertMsg) ? null : kSoundFileMsg);
            string payloadStr = getSipPayloadSilentTest(alertMsg, soundFile, badgeNum, from, msgType);
            byte[] apnPacket = ApnsKit.buildPacket(payloadStr, pushToken, packetID, expiryTime);

            //原始憑證推播
            for (int tryTimes = 1; tryTimes <= 3; tryTimes++)
            {
                try
                {
                    ConnectAPNsAppStore(isDev);

                    //【02】Send Data Packet
                    //
                    SslStream apnsStream = (isDev ? apnsStreamSandboxAppStore : apnsStreamProdAppStore);

                    apnsStream.Write(apnPacket);
                    apnsStream.Flush();

                    break;
                }
                catch (System.IO.IOException ex)
                {
                    if (ex.InnerException is SocketException)
                    {
                        SocketException sEx = (SocketException)ex.InnerException;

                        string debugStr = string.Format("({0}) tryTimes={1};", packetID, tryTimes);
                        debugStr += Environment.NewLine + ChrisKit.Ex2Xml(sEx);
                        debugStr += Environment.NewLine + Environment.NewLine;
                        Kit.ChrisKit.AppendToDayLog(debugStr);

                        if (sEx.SocketErrorCode == SocketError.ConnectionAborted)//SocketError=10053
                            continue;
                        else if (sEx.SocketErrorCode == SocketError.TimedOut)//SocketError=10060
                            continue;
                        else if (sEx.SocketErrorCode == SocketError.WouldBlock)//SocketError=10035
                            break;
                        else
                        {
                            continue;
                        }
                    }
                }
                catch (Exception ex)
                {
                    string errorMsg = Kit.ChrisKit.Ex2Xml(ex, true);
                    Kit.ChrisKit.AppendToDayLog(errorMsg);
                    Kit.ChrisKit.AppendToDayLog("[Payload]=" + payloadStr);
                }
            }//End of for
        }

        /// <summary>
        /// 連接 APNs Server
        /// </summary>
        /// <param name="isDev">是否為開發環境</param>
        private static void ConnectAPNsInHouse(bool isDev)
        {
            CloseConnAPNsInHouse(isDev);

            if (isDev)
            {
                apnsClientSandboxInHouse = new TcpClient(kApnsHostSandbox, kApnsPort);
                apnsClientSandboxInHouse.SendTimeout = kSendTimeout;
                apnsClientSandboxInHouse.ReceiveTimeout = kReceiveTimeout;
                //X509Certificate2 certSandbox = new X509Certificate2(kP12FileSandbox, kP12PasswordSandbox);
                X509Certificate2 certSandbox = new X509Certificate2(GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PATH, CertificateVersion.SANDBOX),
                    GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PASSWORD, CertificateVersion.SANDBOX));     //改寫成讀資料庫

                X509Certificate2Collection certsSandbox = new X509Certificate2Collection(certSandbox);
                apnsStreamSandboxInHouse = new SslStream(apnsClientSandboxInHouse.GetStream());
                apnsStreamSandboxInHouse.AuthenticateAsClient(kApnsHostSandbox, certsSandbox, SslProtocols.Tls, false);
            }
            else
            {
                apnsClientProdInHouse = new TcpClient(kApnsHostProduction, kApnsPort);
                apnsClientProdInHouse.SendTimeout = kSendTimeout;
                apnsClientProdInHouse.ReceiveTimeout = kReceiveTimeout;
                //X509Certificate2 certProd = new X509Certificate2(kP12FileProd, kP12PasswordProd);
                X509Certificate2 certProd = new X509Certificate2(GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PATH, CertificateVersion.PROD),
                    GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PASSWORD, CertificateVersion.PROD));     //改寫成讀資料庫

                X509Certificate2Collection certsProd = new X509Certificate2Collection(certProd);
                apnsStreamProdInHouse = new SslStream(apnsClientProdInHouse.GetStream());
                apnsStreamProdInHouse.AuthenticateAsClient(kApnsHostProduction, certsProd, SslProtocols.Tls, false);
            }
        }

        /// <summary>
        /// 連接 APNs Server
        /// </summary>
        /// <param name="isDev">是否為開發環境</param>
        private static void ConnectAPNsAppStore(bool isDev)
        {
            CloseConnAPNsInHouse(isDev);

            if (isDev)
            {
                apnsClientSandboxAppStore = new TcpClient(kApnsHostSandbox, kApnsPort);
                apnsClientSandboxAppStore.SendTimeout = kSendTimeout;
                apnsClientSandboxAppStore.ReceiveTimeout = kReceiveTimeout;
                //X509Certificate2 certSandbox = new X509Certificate2(kP12FileSandbox2, kP12PasswordSandbox2);
                X509Certificate2 certSandbox = new X509Certificate2(GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PATH, CertificateVersion.SANDBOX),
                    GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PASSWORD, CertificateVersion.SANDBOX));     //改寫成讀資料庫

                X509Certificate2Collection certsSandbox = new X509Certificate2Collection(certSandbox);
                apnsStreamSandboxAppStore = new SslStream(apnsClientSandboxAppStore.GetStream());
                apnsStreamSandboxAppStore.AuthenticateAsClient(kApnsHostSandbox, certsSandbox, SslProtocols.Tls, false);
            }
            else
            {
                apnsClientProdAppStore = new TcpClient(kApnsHostProduction, kApnsPort);
                apnsClientProdAppStore.SendTimeout = kSendTimeout;
                apnsClientProdAppStore.ReceiveTimeout = kReceiveTimeout;
                //X509Certificate2 certProd = new X509Certificate2(kP12FileProd2, kP12PasswordProd2);
                X509Certificate2 certProd = new X509Certificate2(GetCertificateConfig(CertificateType.APPSTORE, ConfigStringType.PATH, CertificateVersion.PROD),
                    GetCertificateConfig(CertificateType.APPSTORE, ConfigStringType.PASSWORD, CertificateVersion.PROD));     //改寫成讀資料庫


                X509Certificate2Collection certsProd = new X509Certificate2Collection(certProd);
                apnsStreamProdAppStore = new SslStream(apnsClientProdAppStore.GetStream());
                apnsStreamProdAppStore.AuthenticateAsClient(kApnsHostProduction, certsProd, SslProtocols.Tls, false);
            }
        }

        /// <summary>
        /// 初始化 APNS 連線
        /// </summary>
        public static void OpenConnection()
        {
            if (!System.IO.File.Exists(GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PATH, CertificateVersion.PROD)))
                throw new Exception(GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PATH, CertificateVersion.PROD) + " not found!");
            if (!System.IO.File.Exists(GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PATH, CertificateVersion.SANDBOX)))
                throw new Exception(GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PATH, CertificateVersion.SANDBOX) + " not found!");
            if (!System.IO.File.Exists(GetCertificateConfig(CertificateType.APPSTORE, ConfigStringType.PATH, CertificateVersion.PROD)))
                throw new Exception(GetCertificateConfig(CertificateType.APPSTORE, ConfigStringType.PATH, CertificateVersion.PROD) + " not found!");
            if (!System.IO.File.Exists(GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PATH, CertificateVersion.SANDBOX)))
                throw new Exception(GetCertificateConfig(CertificateType.INHOUSE, ConfigStringType.PATH, CertificateVersion.SANDBOX) + " not found!");

            ConnectAPNsInHouse(false);
            ConnectAPNsInHouse(true);
            ConnectAPNsAppStore(false);
            ConnectAPNsAppStore(true);
        }

        /// <summary>
        /// 關閉 APNS 連線
        /// </summary>
        /// <param name="isDev">是否為開發環境</param>
        private static void CloseConnAPNsInHouse(bool isDev)
        {
            if (isDev)
            {
                if (apnsClientSandboxInHouse != null)
                {
                    apnsStreamSandboxInHouse.Close();
                    apnsClientSandboxInHouse.Close();
                    apnsStreamSandboxInHouse = null;
                    apnsClientSandboxInHouse = null;
                }
            }
            else
            {
                if (apnsClientProdInHouse != null)
                {
                    apnsStreamProdInHouse.Close();
                    apnsClientProdInHouse.Close();
                    apnsStreamProdInHouse = null;
                    apnsClientProdInHouse = null;
                }
            }
        }

        /// <summary>
        /// 關閉 APNS 連線
        /// </summary>
        /// <param name="isDev">是否為開發環境</param>
        private static void CloseConnAPNsAppStore(bool isDev)
        {
            if (isDev)
            {
                if (apnsClientSandboxAppStore != null)
                {
                    apnsStreamSandboxAppStore.Close();
                    apnsClientSandboxAppStore.Close();
                    apnsStreamSandboxAppStore = null;
                    apnsClientSandboxAppStore = null;
                }
            }
            else
            {
                if (apnsClientProdAppStore != null)
                {
                    apnsStreamProdAppStore.Close();
                    apnsClientProdAppStore.Close();
                    apnsStreamProdAppStore = null;
                    apnsClientProdAppStore = null;
                }
            }
        }

        /// <summary>
        /// 結束 APNS 連線
        /// </summary>
        public static void CloseConnection()
        {
            CloseConnAPNsInHouse(false);
            CloseConnAPNsInHouse(true);
            CloseConnAPNsAppStore(false);
            CloseConnAPNsAppStore(true);
        }

        /// <summary>
        /// 產生 APNs Payload JSON String
        /// </summary>
        /// <param name="alertMsg">alert文字訊息</param>
        /// <param name="soundFile">提示音效檔名</param>
        /// <param name="badgeNum">App badge number</param>
        /// <param name="from">發訊方ID</param>
        /// <param name="msgType">MsgType代碼</param>
        /// <returns>Payload JSON String</returns>
        private static string getSipPayload(string alertMsg, string soundFile, UInt32? badgeNum, string from, string msgType)
        {
            ApsPayload apsPayload = new ApsPayload(alertMsg, badgeNum, soundFile);
            CCPSIPPayload payloadObj = new CCPSIPPayload(apsPayload, from, msgType);

            fastJSON.JSON.Instance.Parameters.EnableAnonymousTypes = true;
            fastJSON.JSON.Instance.Parameters.SerializeNullValues = false;
            return fastJSON.JSON.Instance.ToJSON(payloadObj);
        }

        /// <summary>
        /// 產生 APNs Payload JSON String
        /// </summary>
        /// <param name="alertMsg">alert文字訊息</param>
        /// <param name="soundFile">提示音效檔名</param>
        /// <param name="badgeNum">App badge number</param>
        /// <param name="from">發訊方ID</param>
        /// <param name="msgType">MsgType代碼</param>
        /// <returns>Payload JSON String</returns>
        private static string getSipPayloadSilentTest(string alertMsg, string soundFile, UInt32? badgeNum, string from, string msgType)
        {
            ApsPayloadSilentTest apsPayload = new ApsPayloadSilentTest(alertMsg, badgeNum, soundFile);
            CCPSIPPayloadSilentTest payloadObj = new CCPSIPPayloadSilentTest(apsPayload, from, msgType);

            fastJSON.JSON.Instance.Parameters.EnableAnonymousTypes = true;
            fastJSON.JSON.Instance.Parameters.SerializeNullValues = false;
            return fastJSON.JSON.Instance.ToJSON(payloadObj);
        }

        /// <summary>
        /// Build APNs binary packet
        /// </summary>
        /// <param name="Payload">Payload JSON String</param>
        /// <param name="DeviceToken">Device Push Token</param>
        /// <param name="PacketID">Notification ID (for reporting error)</param>
        /// <param name="ExpiryTime">Push通知過期時間</param>
        /// <returns>APNs binary packet</returns>
        private static byte[] buildPacket(string Payload, string DeviceToken, UInt32 PacketID, DateTime ExpiryTime)
        {
            int payloadLength = Encoding.UTF8.GetByteCount(Payload);
            if (payloadLength > 256)
            {
                return null;
            }

            byte[] buffer = new byte[payloadLength + 45];
            buffer[0] = 1; //Command byte

            // 【Notification identifier】
            //
            byte[] packetIDBytes = BitConverter.GetBytes(PacketID);
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(packetIDBytes);
            }
            for (int i = 0; i < 4; i++)
            {
                buffer[1 + i] = packetIDBytes[i];
            }

            // 【Expiry time】(in Unix time)
            //
            TimeSpan expiryTimeSpan = ExpiryTime.ToUniversalTime() - new DateTime(1970, 1, 1);
            int expiryUnixTime = Convert.ToInt32(expiryTimeSpan.TotalSeconds);
            byte[] expiryTimeBytes = BitConverter.GetBytes(expiryUnixTime);
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(expiryTimeBytes);
            }
            for (int i = 0; i < 4; i++)
            {
                buffer[5 + i] = expiryTimeBytes[i];
            }


            buffer[9] = 0; //Device token length
            buffer[10] = 32; //Device token length
            for (int i = 0; i < 32; i++)
            {
                buffer[11 + i] = Convert.ToByte(DeviceToken.Substring(i * 2, 2), 16);
            }

            if (payloadLength < 256)
            {
                buffer[43] = 0;
                buffer[44] = Convert.ToByte(payloadLength);
            }
            else
            {//payloadLength == 256
                buffer[43] = 1;
                buffer[44] = 0;
            }

            Encoding.UTF8.GetBytes(Payload, 0, Payload.Length, buffer, 45);

            return buffer;
        }

        /// <summary>
        /// CCPSIP Payload 物件
        /// </summary>
        [Serializable()]
        private class CCPSIPPayload
        {
            private ApsPayload _aps;
            private string _from;
            private string _msgType;

            public ApsPayload aps { get { return this._aps; } }
            public string from { get { return this._from; } }
            public string msgType { get { return this._msgType; } }

            public CCPSIPPayload(ApsPayload payload, string From, string MsgType)
            {
                _aps = payload;
                _from = From;
                _msgType = MsgType;
            }
        }

        /// <summary>
        /// CCPSIP Payload Silent Test物件
        /// </summary>
        [Serializable()]
        private class CCPSIPPayloadSilentTest
        {
            private ApsPayloadSilentTest _aps;
            private string _from;
            private string _msgType;

            public ApsPayloadSilentTest aps { get { return this._aps; } }
            public string from { get { return this._from; } }
            public string msgType { get { return this._msgType; } }

            public CCPSIPPayloadSilentTest(ApsPayloadSilentTest payload, string From, string MsgType)
            {
                _aps = payload;
                _from = From;
                _msgType = MsgType;
            }
        }

        /// <summary>
        /// aps Payload 物件
        /// </summary>
        [Serializable()]
        private class ApsPayload
        {
            private string _alert;
            private UInt32? _badge;
            private string _sound;

            public string alert { get { return this._alert; } }
            public UInt32? badge { get { return this._badge; } }
            public string sound { get { return this._sound; } }

            public ApsPayload(string alertStr, UInt32? badgeNum, string soundFile)
            {
                _alert = alertStr;
                _badge = badgeNum;
                _sound = soundFile;
            }
        }

        /// <summary>
        /// aps Payload Silent Test 物件
        /// </summary>
        [Serializable()]
        private class ApsPayloadSilentTest
        {
            private string _alert;
            private UInt32? _badge;
            private string _sound;
            private string _content_available;

            public string alert { get { return this._alert; } }
            public UInt32? badge { get { return this._badge; } }
            public string sound { get { return this._sound; } }
            public string context_available { get { return this._content_available; } }

            public ApsPayloadSilentTest(string alertStr, UInt32? badgeNum, string soundFile)
            {
                _alert = alertStr;
                _badge = badgeNum;
                _sound = "";
                _content_available = "1";
            }
        }

    }
}